Two-Dimensional Moment Frame Analysis

This is a complete example of frame analysis. Although it is a 2D case, it can be applied to more complex 3D models. While it demonstrates linear elastic analysis, it is also applicable to nonlinear elastoplastic analysis.

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import numpy as np
import openseespy.opensees as ops
from matplotlib.colors import ListedColormap

import opstool as opst
import opstool.vis.pyvista as opsvis

Create the model

def FEModel():
    ops.wipe()
    ops.model("basic", "-ndm", 2, "-ndf", 3)

    # Defining Nodes

    ops.node(1, 0.000000e00, 0.000000e00)
    ops.node(2, 3.600000e02, 0.000000e00)
    ops.node(3, 7.200000e02, 0.000000e00)
    ops.node(4, 0.000000e00, 1.620000e02)
    ops.node(5, 3.600000e02, 1.620000e02)
    ops.node(6, 7.200000e02, 1.620000e02)
    ops.node(7, 0.000000e00, 3.240000e02)
    ops.node(8, 3.600000e02, 3.240000e02)
    ops.node(9, 7.200000e02, 3.240000e02)
    ops.node(10, 0.000000e00, 4.800000e02)
    ops.node(11, 3.600000e02, 4.800000e02)
    ops.node(12, 7.200000e02, 4.800000e02)
    ops.node(13, 0.000000e00, 6.360000e02)
    ops.node(14, 3.600000e02, 6.360000e02)
    ops.node(15, 7.200000e02, 6.360000e02)
    ops.node(16, 0.000000e00, 7.920000e02)
    ops.node(17, 3.600000e02, 7.920000e02)
    ops.node(18, 7.200000e02, 7.920000e02)
    ops.node(19, 0.000000e00, 9.480000e02)
    ops.node(20, 3.600000e02, 9.480000e02)
    ops.node(21, 7.200000e02, 9.480000e02)
    ops.node(22, 0.000000e00, 1.104000e03)
    ops.node(23, 3.600000e02, 1.104000e03)
    ops.node(24, 7.200000e02, 1.104000e03)

    # Write node restraint

    ops.fix(1, 1, 1, 1)
    ops.fix(2, 1, 1, 1)
    ops.fix(3, 1, 1, 1)

    # Define the rigidDiaphragm
    ops.rigidDiaphragm(1, 5, 4, 6)
    ops.rigidDiaphragm(1, 8, 7, 9)
    ops.rigidDiaphragm(1, 11, 10, 12)
    ops.rigidDiaphragm(1, 14, 13, 15)
    ops.rigidDiaphragm(1, 17, 16, 18)
    ops.rigidDiaphragm(1, 20, 19, 21)
    ops.rigidDiaphragm(1, 23, 22, 24)

    # Defining Frame Elements

    ops.geomTransf("Linear", 1)
    ops.element("elasticBeamColumn", 1, 1, 4, 7.230000e01, 2.950000e04, 3.230000e03, 1)
    ops.element("elasticBeamColumn", 2, 4, 7, 7.230000e01, 2.950000e04, 3.230000e03, 1)
    ops.element("elasticBeamColumn", 3, 7, 10, 7.230000e01, 2.950000e04, 3.230000e03, 1)
    ops.element("elasticBeamColumn", 4, 10, 13, 6.210000e01, 2.950000e04, 2.670000e03, 1)
    ops.element("elasticBeamColumn", 5, 13, 16, 6.210000e01, 2.950000e04, 2.670000e03, 1)
    ops.element("elasticBeamColumn", 6, 16, 19, 5.170000e01, 2.950000e04, 2.150000e03, 1)
    ops.element("elasticBeamColumn", 7, 19, 22, 5.170000e01, 2.950000e04, 2.150000e03, 1)
    ops.element("elasticBeamColumn", 8, 2, 5, 8.440000e01, 2.950000e04, 3.910000e03, 1)
    ops.element("elasticBeamColumn", 9, 5, 8, 8.440000e01, 2.950000e04, 3.910000e03, 1)
    ops.element("elasticBeamColumn", 10, 8, 11, 8.440000e01, 2.950000e04, 3.910000e03, 1)
    ops.element("elasticBeamColumn", 11, 11, 14, 7.230000e01, 2.950000e04, 3.230000e03, 1)
    ops.element("elasticBeamColumn", 12, 14, 17, 7.230000e01, 2.950000e04, 3.230000e03, 1)
    ops.element("elasticBeamColumn", 13, 17, 20, 6.210000e01, 2.950000e04, 2.670000e03, 1)
    ops.element("elasticBeamColumn", 14, 20, 23, 6.210000e01, 2.950000e04, 2.670000e03, 1)
    ops.element("elasticBeamColumn", 15, 3, 6, 7.230000e01, 2.950000e04, 3.230000e03, 1)
    ops.element("elasticBeamColumn", 16, 6, 9, 7.230000e01, 2.950000e04, 3.230000e03, 1)
    ops.element("elasticBeamColumn", 17, 9, 12, 7.230000e01, 2.950000e04, 3.230000e03, 1)
    ops.element("elasticBeamColumn", 18, 12, 15, 6.210000e01, 2.950000e04, 2.670000e03, 1)
    ops.element("elasticBeamColumn", 19, 15, 18, 6.210000e01, 2.950000e04, 2.670000e03, 1)
    ops.element("elasticBeamColumn", 20, 18, 21, 5.170000e01, 2.950000e04, 2.150000e03, 1)
    ops.element("elasticBeamColumn", 21, 21, 24, 5.170000e01, 2.950000e04, 2.150000e03, 1)
    ops.element("elasticBeamColumn", 22, 4, 5, 4.710000e01, 2.950000e04, 5.120000e03, 1)
    ops.element("elasticBeamColumn", 23, 7, 8, 4.710000e01, 2.950000e04, 5.120000e03, 1)
    ops.element("elasticBeamColumn", 24, 10, 11, 3.830000e01, 2.950000e04, 4.020000e03, 1)
    ops.element("elasticBeamColumn", 25, 13, 14, 3.830000e01, 2.950000e04, 4.020000e03, 1)
    ops.element("elasticBeamColumn", 26, 16, 17, 3.250000e01, 2.950000e04, 3.330000e03, 1)
    ops.element("elasticBeamColumn", 27, 19, 20, 3.250000e01, 2.950000e04, 3.330000e03, 1)
    ops.element("elasticBeamColumn", 28, 22, 23, 3.250000e01, 2.950000e04, 3.330000e03, 1)
    ops.element("elasticBeamColumn", 29, 5, 6, 4.710000e01, 2.950000e04, 5.120000e03, 1)
    ops.element("elasticBeamColumn", 30, 8, 9, 4.710000e01, 2.950000e04, 5.120000e03, 1)
    ops.element("elasticBeamColumn", 31, 11, 12, 3.830000e01, 2.950000e04, 4.020000e03, 1)
    ops.element("elasticBeamColumn", 32, 14, 15, 3.830000e01, 2.950000e04, 4.020000e03, 1)
    ops.element("elasticBeamColumn", 33, 17, 18, 3.250000e01, 2.950000e04, 3.330000e03, 1)
    ops.element("elasticBeamColumn", 34, 20, 21, 3.250000e01, 2.950000e04, 3.330000e03, 1)
    ops.element("elasticBeamColumn", 35, 23, 24, 3.250000e01, 2.950000e04, 3.330000e03, 1)

    # Define the mass
    ops.mass(5, 0.49, 0.0, 0.0)
    ops.mass(8, 0.49, 0.0, 0.0)
    ops.mass(11, 0.49, 0.0, 0.0)
    ops.mass(14, 0.49, 0.0, 0.0)
    ops.mass(17, 0.49, 0.0, 0.0)
    ops.mass(20, 0.49, 0.0, 0.0)
    ops.mass(23, 0.49, 0.0, 0.0)


FEModel()

Let’s first visualize the geometry by Pyvista:

opsvis.set_plot_props(line_width=4, point_size=2)
plotter = opsvis.plot_model(show_ele_numbering=True, show_node_numbering=True)
plotter.show()
ex frame2D

You can also use the following code to visualize the model by using Plotly:

opst.vis.plotly.set_plot_props(line_width=4, point_size=2)
fig = opst.vis.plotly.plot_model()
fig
# fig.write_html("model.html")
# fig.show(renderer="browser")


Eigenvalue analysis

solver=”-fullGenLapack” is intended to extract all seventh-order modes. This solver should be avoided in actual large models.

opst.post.save_eigen_data(odb_tag="eigen", mode_tag=6, solver="-fullGenLapack")
WARNING - the 'fullGenLapack' eigen solver is VERY SLOW. Consider using the default eigen solver.Using DomainModalProperties - Developed by: Massimo Petracca, Guido Camata, ASDEA Software Technology
OPSTOOL™ ::  Eigen data has been saved to G:\opstool\docs\.opstool.output/EigenData-eigen.zarr!

odb_tag = “eigen” is used to indentify the output database for eigenvalue analysis.

cmap = ListedColormap(["blue"])
opsvis.set_plot_props(cmap=cmap, line_width=4, point_size=5, font_size=12)
plotter = opsvis.plot_eigen(mode_tags=[1, 6], odb_tag="eigen", subplots=True, bc_scale=3, show_mp_constraint=False)
plotter.show()  # for auto
ex frame2D
OPSTOOL™ ::  Loading eigen data from G:\opstool\docs\.opstool.output/EigenData-eigen.zarr ...

We can also retrieve the data of eigenvalue analysis:

model_props, eigen_vectors = opst.post.get_eigen_data(odb_tag="eigen")
model_props_df = model_props.to_pandas()  # to pandas DataFrame
print(model_props_df.columns)
OPSTOOL™ ::  Loading eigen data from G:\opstool\docs\.opstool.output/EigenData-eigen.zarr ...
Index(['eigenLambda', 'eigenOmega', 'eigenFrequency', 'eigenPeriod',
       'partiFactorMX', 'partiFactorMY', 'partiFactorRMZ', 'partiMassMX',
       'partiMassMY', 'partiMassRMZ', 'partiMassesCumuMX', 'partiMassesCumuMY',
       'partiMassesCumuRMZ', 'partiMassRatiosMX', 'partiMassRatiosMY',
       'partiMassRatiosRMZ', 'partiMassRatiosCumuMX', 'partiMassRatiosCumuMY',
       'partiMassRatiosCumuRMZ'],
      dtype='object', name='Properties')
print("Modal period: ", model_props_df["eigenPeriod"])
print("-" * 50)
print("Participation mass ratio: ", model_props_df["partiMassRatiosMX"])
print("-" * 50)
print("Cumulative participation mass ratio: ", model_props_df["partiMassRatiosCumuMX"])
Modal period:  modeTags
1    1.273211
2    0.431278
3    0.242043
4    0.160179
5    0.118990
6    0.095064
Name: eigenPeriod, dtype: float64
--------------------------------------------------
Participation mass ratio:  modeTags
1    79.962669
2    11.336182
3     4.180994
4     2.115029
5     1.414555
6     0.679967
Name: partiMassRatiosMX, dtype: float64
--------------------------------------------------
Cumulative participation mass ratio:  modeTags
1    79.962669
2    91.298850
3    95.479845
4    97.594874
5    99.009429
6    99.689396
Name: partiMassRatiosCumuMX, dtype: float64

Static analysis

# Defining Lateral Distributed Loads:
FEModel()
# Define the load pattern
ops.timeSeries("Linear", 1)
ops.pattern("Plain", 1, 1)
ops.load(4, 2.5, 0.0, 0.0)
ops.load(7, 5.0, 0.0, 0.0)
ops.load(10, 7.5, 0.0, 0.0)
ops.load(13, 10.0, 0.0, 0.0)
ops.load(16, 12.5, 0.0, 0.0)
ops.load(19, 15.0, 0.0, 0.0)
ops.load(22, 20.0, 0.0, 0.0)

Re-examine the model:

opsvis.set_plot_props(line_width=4, point_size=4, font_size=12)
fig = opsvis.plot_model(show_nodal_loads=True, load_scale=2, bc_scale=3)
fig.show()  # fig.show() for practical use
ex frame2D

To perform the analysis:

n_steps = 10
ops.system("BandGeneral")
ops.constraints("Transformation")
ops.numberer("RCM")
ops.test("NormDispIncr", 1.0e-12, 10, 3)
ops.algorithm("Linear")
ops.integrator("LoadControl", 1 / n_steps)
ops.analysis("Static")

Save the results

odb = opst.post.CreateODB(odb_tag="static", interpolate_beam_disp=11)
for _ in range(n_steps):
    ops.analyze(1)  # one step of analysis
    odb.fetch_response_step()  # fetch the response on the current step
odb.save_response()
OPSTOOL™ ::  All responses data with _odb_tag = static saved in
G:\opstool\docs\.opstool.output\RespStepData-static.odb!

Retrieve the nodal displacements:

node_resp = opst.post.get_nodal_responses(odb_tag="static")
print(
    "Node 22 displacement in x direction: ",
    node_resp["disp"].sel(nodeTags=22, DOFs="UX").data[-1],
)
OPSTOOL™ ::  Loading all response data from G:\opstool\docs\.opstool.output\RespStepData-static.odb ...
Node 22 displacement in x direction:  1.450757

Retrieve element response:

ele_resp = opst.post.get_element_responses(odb_tag="static", ele_type="Frame")
print(ele_resp.data_vars)
OPSTOOL™ ::  Loading Frame response data from G:\opstool\docs\.opstool.output\RespStepData-static.odb ...
Data variables:
    basicDeformations    (time, eleTags, basicDofs) float32 9kB 0.0 -0.0 ... 0.0
    basicForces          (time, eleTags, basicDofs) float32 9kB 0.0 -0.0 ... 0.0
    localForces          (time, eleTags, localDofs) float32 18kB -0.0 ... -527.7
    plasticDeformation   (time, eleTags, basicDofs) float32 9kB 0.0 -0.0 ... 0.0
    sectionDeformations  (time, eleTags, secPoints, secDofs) float32 65kB 0.0...
    sectionForces        (time, eleTags, secPoints, secDofs) float32 65kB -0....
    sectionLocs          (time, eleTags, secPoints, locs) float32 32kB 0.0 .....
ele_forces = ele_resp["sectionForces"]
print("M:", ele_forces.sel(eleTags=1, secDofs="MZ", secPoints=1).data)
print("V:", ele_forces.sel(eleTags=1, secDofs="VY", secPoints=1).data)
print("N:", ele_forces.sel(eleTags=1, secDofs="N", secPoints=1).data)
M: [  -0.       232.46773  464.93546  697.4032   929.8709  1162.3386
 1394.8064  1627.274   1859.7418  2092.2095  2324.6772 ]
V: [ -0.         -2.067208   -4.134416   -6.2016244  -8.268832  -10.3360405
 -12.403249  -14.470457  -16.537664  -18.604874  -20.672081 ]
N: [-0.         6.9986734 13.997347  20.99602   27.994694  34.993366
 41.99204   48.990715  55.989388  62.98806   69.98673  ]

Visualize node responses:

opsvis.set_plot_props(cmap="coolwarm", line_width=4, title_font_size=10)
plotter = opsvis.plot_nodal_responses(
    odb_tag="static", resp_type="disp", resp_dof=["UX", "UY"], slides=True, defo_scale=100.0, interpolate_beam_disp=True
)
plotter.show()
ex frame2D
OPSTOOL™ ::  Loading responses data from G:\opstool\docs\.opstool.output\RespStepData-static.odb ...

Visualizing Element Response:

# Moment response
opsvis.set_plot_props(cmap="seismic", line_width=1, title_font_size=14)

opsvis.plot_frame_responses(
    odb_tag="static",
    resp_type="sectionForces",
    resp_dof="Mz",
    unit_symbol="N·mm",
    unit_factor=1.0,
    slides=True,
    scale=-2.0,
    style="wireframe",
).show()
ex frame2D
OPSTOOL™ ::  Loading responses data from G:\opstool\docs\.opstool.output\RespStepData-static.odb ...

Axial response

opsvis.plot_frame_responses(
    odb_tag="static",
    resp_type="sectionForces",
    resp_dof="N",
    slides=True,
    scale=-2.0,
    line_width=3,
    style="wireframe",
).show()
ex frame2D
OPSTOOL™ ::  Loading responses data from G:\opstool\docs\.opstool.output\RespStepData-static.odb ...

Seismic response analysis

Load the ground motion data. All files can be downloaded here: click

gmdata = np.loadtxt("ELCENTRO.txt")
time = gmdata[:, 0]
accel = gmdata[:, 1]

print(len(time))
plt.plot(time, accel)
plt.show()
ex frame2D
1560

Create the ground motion load pattern

FEModel()
ops.timeSeries("Path", 2, "-time", *time, "-values", *accel, "-factor", 386.4)
ops.pattern("UniformExcitation", 2, 1, "-accel", 2)

Create the Rayleigh damping

xDamp = 0.05
MpropSwitch = 1.0
KcurrSwitch = 0.0
KcommSwitch = 1.0
KinitSwitch = 0.0
nEigenI = 1  # mode 1
nEigenJ = 2  # mode 2
lambdaN = ops.eigen(nEigenJ)  # eigenvalue analysis for nEigenJ modes
lambdaI = lambdaN[nEigenI - 1]  # eigenvalue mode i
lambdaJ = lambdaN[nEigenJ - 1]  # eigenvalue mode j
omegaI = np.sqrt(lambdaI)
omegaJ = np.sqrt(lambdaJ)
# M-prop. damping; D = alphaM*M
alphaM = MpropSwitch * xDamp * (2 * omegaI * omegaJ) / (omegaI + omegaJ)
# current-K;      +beatKcurr*KCurrent
betaKcurr = KcurrSwitch * 2.0 * xDamp / (omegaI + omegaJ)
# last-committed K;   +betaKcomm*KlastCommitt
betaKcomm = KcommSwitch * 2.0 * xDamp / (omegaI + omegaJ)
betaKinit = KinitSwitch * 2.0 * xDamp / (omegaI + omegaJ)
ops.rayleigh(alphaM, 0.0, 0.0, betaKcomm)

Perform analysis and save results

ops.wipeAnalysis()
ops.system("BandGeneral")
ops.constraints("Transformation")
ops.numberer("RCM")
ops.test("NormDispIncr", 1.0e-12, 10, 3)
ops.algorithm("Linear")
ops.integrator("HHT", 1.0, 0.5, 0.25)
ops.analysis("Transient")
n_steps, dt = 1600, 0.02
odb = opst.post.CreateODB(odb_tag="seismic", interpolate_beam_disp=11)
for _ in range(n_steps):
    ops.analyze(1, dt)
    odb.fetch_response_step()
odb.save_response()
# zlib=True to compress the data
OPSTOOL™ ::  All responses data with _odb_tag = seismic saved in
G:\opstool\docs\.opstool.output\RespStepData-seismic.odb!

Retrieve Node Responses

node_resp = opst.post.get_nodal_responses(odb_tag="seismic")
node_disp22 = node_resp["disp"].sel(nodeTags=22, DOFs="UX")
node_vel22 = node_resp["vel"].sel(nodeTags=22, DOFs="UX")
node_accel22 = node_resp["accel"].sel(nodeTags=22, DOFs="UX")
OPSTOOL™ ::  Loading all response data from G:\opstool\docs\.opstool.output\RespStepData-seismic.odb ...
fig, axes = plt.subplots(3, 1, sharex=True)
time = node_disp22.time

# Define colors and line styles
colors = ["#136ad5", "#fb8a2e"]  # Use neutral colors for publication standards
line_styles = ["-", "--"]  # Solid and dashed lines for differentiation

# Plot data with clear labels
axes[0].plot(time, node_disp22.data, color=colors[0], linestyle=line_styles[0], linewidth=1.5)

axes[1].plot(time, node_vel22.data, color=colors[0], linestyle=line_styles[0], linewidth=1.5)

axes[2].plot(time, node_accel22.data, color=colors[0], linestyle=line_styles[0], linewidth=1.5)

# Set axis labels and title font sizes
axes[0].set_ylabel("u", fontsize=10)
axes[1].set_ylabel("v", fontsize=10)
axes[2].set_ylabel("a", fontsize=10)
axes[2].set_xlabel("Time (s)", fontsize=10)

# Customize each subplot
for ax in axes:
    ax.tick_params(axis="both", which="major", labelsize=8)  # Set tick font size
    ax.grid(True, linestyle="--", linewidth=0.5, alpha=0.7)  # Add light grid lines
    # ax.legend(fontsize=9, loc="best", frameon=False)  # Simple legend without box

# Format X-axis ticks to have consistent significant figures
for ax in axes:
    ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f"))

# Adjust spacing between subplots
fig.subplots_adjust(hspace=0.2)  # Adjust vertical spacing
# Save figure in a publication-friendly format
# plt.savefig("fig-node22resp.pdf", bbox_inches="tight")
plt.show()
ex frame2D
print("OpenSees Node 22 Disp Max:", node_disp22.data.max())
print("OpenSees Node 22 Vel Max:", node_vel22.data.max())
print("OpenSees Node 22 Accel Max:", node_accel22.data.max())
print("-" * 50)
print("OpenSees Node 22 Disp Min:", node_disp22.data.min())
print("OpenSees Node 22 Vel Min:", node_vel22.data.min())
print("OpenSees Node 22 Accel Min:", node_accel22.data.min())
OpenSees Node 22 Disp Max: 4.886218
OpenSees Node 22 Vel Max: 24.562452
OpenSees Node 22 Accel Max: 284.1298
--------------------------------------------------
OpenSees Node 22 Disp Min: -4.141897
OpenSees Node 22 Vel Min: -25.271881
OpenSees Node 22 Accel Min: -249.5041

Retrieve Element Responses

ele_resp = opst.post.get_element_responses(odb_tag="seismic", ele_type="Frame", resp_type="sectionForces")
frame1Mz = -ele_resp.sel(eleTags=1, secDofs="MZ", secPoints=1)
frame1N = ele_resp.sel(eleTags=1, secDofs="N", secPoints=1)
frame1Vy = ele_resp.sel(eleTags=1, secDofs="VY", secPoints=1)
OPSTOOL™ ::  Loading Frame sectionForces response data from
G:\opstool\docs\.opstool.output\RespStepData-seismic.odb ...
fig, axes = plt.subplots(3, 1, sharex=True)
time = frame1Mz.time

# Define colors and line styles
colors = ["#136ad5", "#fb8a2e"]  # Use neutral colors for publication standards
line_styles = ["-", "--"]  # Solid and dashed lines for differentiation

# Plot data with clear labels
axes[0].plot(time, frame1N.data, color=colors[0], linestyle=line_styles[0], linewidth=1.5)

axes[1].plot(time, frame1Vy.data, color=colors[0], linestyle=line_styles[0], linewidth=1.5)

axes[2].plot(time, frame1Mz.data, color=colors[0], linestyle=line_styles[0], linewidth=1.5)

# Set axis labels and title font sizes
axes[0].set_ylabel("N", fontsize=10)
axes[1].set_ylabel("V", fontsize=10)
axes[2].set_ylabel("M", fontsize=10)
axes[2].set_xlabel("Time (s)", fontsize=10)

# Customize each subplot
for ax in axes:
    ax.tick_params(axis="both", which="major", labelsize=8)  # Set tick font size
    ax.grid(True, linestyle="--", linewidth=0.5, alpha=0.7)  # Add light grid lines
    # ax.legend(fontsize=9, loc="best", frameon=False)  # Simple legend without box

# Format X-axis ticks to have consistent significant figures
for ax in axes:
    ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f"))

# Adjust spacing between subplots
fig.subplots_adjust(hspace=0.2)  # Adjust vertical spacing
# Save figure in a publication-friendly format
# plt.savefig("fig-frame1-forces.pdf", bbox_inches="tight")
plt.show()
ex frame2D
print("OpenSees Frame 1 N Max:", frame1N.data.max())
print("OpenSees Frame 1 Vy Max:", frame1Vy.data.max())
print("OpenSees Frame 1 Mz Max:", frame1Mz.data.max())
print("-" * 50)
print("OpenSees Frame 1 N Min:", frame1N.data.min())
print("OpenSees Frame 1 Vy Min:", frame1Vy.data.min())
print("OpenSees Frame 1 Mz Min:", frame1Mz.data.min())
OpenSees Frame 1 N Max: 233.99113
OpenSees Frame 1 Vy Max: 61.886253
OpenSees Frame 1 Mz Max: 6995.9204
--------------------------------------------------
OpenSees Frame 1 N Min: -196.78415
OpenSees Frame 1 Vy Min: -71.202286
OpenSees Frame 1 Mz Min: -8003.0625

Creating an .MP4 animation

opsvis.set_plot_props(font_size=8, title_font_size=10, line_width=5, point_size=3, cmap=["blue"])
plotter = opsvis.plot_nodal_responses_animation(
    odb_tag="seismic",
    resp_type="disp",
    resp_dof=["UX", "UY"],
    framerate=200,  # Frames per second
    savefig="NodalRespAnimation.gif",  # or mp4
    defo_scale=10.0,
    interpolate_beam_disp=True,
)
plotter.close()
ex frame2D
OPSTOOL™ ::  Loading responses data from G:\opstool\docs\.opstool.output\RespStepData-seismic.odb ...
Animation has been saved to NodalRespAnimation.gif!

Total running time of the script: (2 minutes 9.919 seconds)

Gallery generated by Sphinx-Gallery